/**
 * This file is mostly a copy of https://github.com/alexandermendes/jest-amp/blob/master/src/index.js.
 *
 * However, as that package does not support TypeScript and has API that is not
 * compatible with react-helmet-async (which is used to add AMP scripts to the
 * head) we've opted to just copy-pasted it with acknowledgement to the original.
 */

import { printReceived, matcherHint } from "jest-matcher-utils";
import diffableHtml from "diffable-html";
import ampHtmlValidator, {
  Validator,
  ValidationResult,
  ValidationError,
} from "amphtml-validator";

import ampTemplate from "./amp-template";

const lineBreak = "\n\n";
const horizontalLine = "----------";

let validator: Validator;

/**
 * Params for {@link toBeValidAmpHtml} jest Matcher.
 */
interface AMPHTMLMatcher {
  /**
   * The result of validation from Validator.validateString(html).
   */
  result: ValidationResult;

  /**
   * The HTML body.
   */
  body: string;

  /**
   * The possible validation errors.
   */
  validationError: ValidationError;
}

/**
 * Helper function to get the instance of the AmpHtmlValidator.
 *
 * @returns Instance of AmpHtmlValidator.
 */
const getValidator: () => Promise<Validator> = async () => {
  if (!validator) {
    validator = await ampHtmlValidator.getInstance();
  }

  return validator;
};

/**
 * Error message formatter.
 *
 * @param errors - Array of errors generated by ampHtmlValidator.
 *
 * @returns The error messages as formatted into a string.
 */
const formatErrors = (errors: Array<ValidationError>) => {
  if (errors.length === 0) {
    return [];
  }

  return errors
    .map((error) => [printReceived(error.message), error.specUrl].join("\n"))
    .join(lineBreak);
};

/**
 * Check if an HTML string conforms to the AMP specification.
 *
 * @param head - A string to be injected in the <head> of the AMP template.
 * @param body - A string to be injected in the <body> of the AMP template.
 * @returns An object describing the validation result.
 */
export const amp = async (body = "", head = "") => {
  const html = ampTemplate({ head, body });

  let result: ValidationResult;
  let validationError: ValidationError;

  try {
    result = (await getValidator()).validateString(html);
  } catch (err) {
    validationError = err;
  }

  return {
    result,
    body,
    validationError,
  };
};

/**
 * The jest matcher that is passed to expect.extend() in order to create a
 * custom jest matcher.
 *
 * @example
 * ```
 * jest.extends({ toBeValidAmpHtml })
 * ```
 *
 * @param params - Defined in {@link AMPHTMLMatcher}.
 *
 * @returns The jest Matcher object.
 */
export const toBeValidAmpHtml = ({
  result,
  body,
  validationError,
}: AMPHTMLMatcher) => {
  const { errors } = result || {};

  if (validationError) {
    throw new Error(
      [
        "Something went wrong while running the AMP validation. ",
        "Please ensure you have an active internet connection.",
        lineBreak,
        validationError.message,
      ].join("")
    );
  }

  if (typeof errors === "undefined") {
    throw new Error("No error report found in AMP validation results");
  }

  const formatedErrors = formatErrors(errors);
  const pass = formatedErrors.length === 0;

  /**
   * A helper function to format the error message.
   *
   * @returns A detailed error message.
   */
  const message = () => {
    return [
      matcherHint(`.${toBeValidAmpHtml.name}`),
      "Expected valid AMP HTML:",
      formatedErrors,
      horizontalLine,
      diffableHtml(body).trim(),
      horizontalLine,
    ].join(lineBreak);
  };

  if (pass) {
    return { pass: true, message: () => "" };
  }

  return {
    actual: errors,
    message,
    pass,
  };
};
